package org.dfl.plugin.editor.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.dfl.core.model.XMLLoaderException;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.jface.viewers.ICellEditorValidator;
import org.eclipse.swt.graphics.Font;
import org.eclipse.ui.views.properties.IPropertyDescriptor;
import org.eclipse.ui.views.properties.PropertyDescriptor;
import org.eclipse.ui.views.properties.TextPropertyDescriptor;
import org.jdom.Element;


/**
 * abstract editor's model element that represents a elements with a shape (place + transitions)
 * @author Piotr Wlodarczyk
 *
 */
public abstract class Shape extends ModelElement implements Serializable {
	/**
	 * A static array of property descriptors. There is one IPropertyDescriptor
	 * entry per editable property.
	 * 
	 * @see #getPropertyDescriptors()
	 * @see #getPropertyValue(Object)
	 * @see #setPropertyValue(Object, Object)
	 */
	private static IPropertyDescriptor[] descriptors;

	/**
	 * ID for the Height property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String HEIGHT_PROP = "Shape.Height";

	/** Property ID to use when the location of this shape is modified. */
	public static final String LOCATION_PROP = "Shape.Location";

	private static final long serialVersionUID = 1;

	/** Property ID to use then the size of this shape is modified. */
	public static final String SIZE_PROP = "Shape.Size";
	
	/** Property ID to use then the size of this shape is modified. */
	public static final String SELF_PROP = "Shape.self";

	/** Property ID to use when the list of outgoing connections is modified. */
	public static final String SOURCE_CONNECTIONS_PROP = "Shape.SourceConn";

	/** Property ID to use when the list of incoming connections is modified. */
	public static final String TARGET_CONNECTIONS_PROP = "Shape.TargetConn";

	/**
	 * ID for the Width property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String WIDTH_PROP = "Shape.Width";

	/**
	 * ID for the X property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String XPOS_PROP = "Shape.xPos";

	/**
	 * ID for the Y property value (used for by the corresponding property
	 * descriptor).
	 */
	private static final String YPOS_PROP = "Shape.yPos";

	/*
	 * Initializes the property descriptors array.
	 * 
	 * @see #getPropertyDescriptors()
	 * @see #getPropertyValue(Object)
	 * @see #setPropertyValue(Object, Object)
	 */
	static {
		descriptors = new IPropertyDescriptor[] {
				new TextPropertyDescriptor(XPOS_PROP, "X"), // id and
															// description pair
				new TextPropertyDescriptor(YPOS_PROP, "Y"),
				new TextPropertyDescriptor(WIDTH_PROP, "Width"),
				new TextPropertyDescriptor(HEIGHT_PROP, "Height"), };
		// use a custom cell editor validator for all four array entries
		for (int i = 0; i < descriptors.length; i++) {
			((PropertyDescriptor) descriptors[i])
					.setValidator(new ICellEditorValidator() {
						public String isValid(Object value) {
							int intValue = -1;
							try {
								intValue = Integer.parseInt((String) value);
							} catch (NumberFormatException exc) {
								return "Not a number";
							}
							return (intValue >= 0) ? null
									: "Value must be >=  0";
						}
					});
		}
	} // static

	/** Location of this shape. */
	private Point location = new Point(0, 0);

	/** Size of this shape. */
	private Dimension size = new Dimension(50, 50);

	/** List of outgoing Connections. */
	private List<ConnectionModelElement> sourceConnections = new ArrayList<ConnectionModelElement>();

	/** List of incoming Connections. */
	private List<ConnectionModelElement> targetConnections = new ArrayList<ConnectionModelElement>();

	/**
	 * Return the Location of this shape.
	 * 
	 * @return a non-null location instance
	 */
	public Point getLocation() {
		return location.getCopy();
	}

	/**
	 * Returns an array of IPropertyDescriptors for this shape.
	 * <p>
	 * The returned array is used to fill the property view, when the edit-part
	 * corresponding to this model element is selected.
	 * </p>
	 * 
	 * @see #descriptors
	 * @see #getPropertyValue(Object)
	 * @see #setPropertyValue(Object, Object)
	 */
	public IPropertyDescriptor[] getPropertyDescriptors() {
		return descriptors;
	}

	/**
	 * Return the property value for the given propertyId, or null.
	 * <p>
	 * The property view uses the IDs from the IPropertyDescriptors array to
	 * obtain the value of the corresponding properties.
	 * </p>
	 * 
	 * @see #descriptors
	 * @see #getPropertyDescriptors()
	 */
	public Object getPropertyValue(Object propertyId) {
		if (XPOS_PROP.equals(propertyId)) {
			return Integer.toString(location.x);
		}
		if (YPOS_PROP.equals(propertyId)) {
			return Integer.toString(location.y);
		}
		if (HEIGHT_PROP.equals(propertyId)) {
			return Integer.toString(size.height);
		}
		if (WIDTH_PROP.equals(propertyId)) {
			return Integer.toString(size.width);
		}
		return super.getPropertyValue(propertyId);
	}

	/**
	 * Return the Size of this shape.
	 * 
	 * @return a non-null Dimension instance
	 */
	public Dimension getSize() {
		Label label = new Label();
		label.setFont(getLabelFont());
		label.setText(getLabelText());
		
//		return size.getCopy();
		Dimension size = label.getPreferredSize();
		size.height += 15; //margins
		size.width += 15;
		size.width = Math.min(size.width, getMaxLabelWidth());
		size.height = Math.min(size.height, getMaxLabelHeight());
		return size;
	}
	
	public abstract String getLabelText();
	public abstract String getTipText();
	public abstract int getMaxLabelWidth();
	public abstract int getMaxLabelHeight();
	protected abstract Font getLabelFont();

	/**
	 * Return a List of outgoing Connections.
	 */
	public List<ConnectionModelElement> getSourceConnections() {
		return sourceConnections;
	}

	/**
	 * Return a List of incoming Connections.
	 */
	public List<ConnectionModelElement> getTargetConnections() {
		return targetConnections;
	}

	/**
	 * Add an incoming or outgoing connection to this shape.
	 * 
	 * @param conn
	 *            a non-null connection instance
	 * @throws Exception 
	 * @throws IllegalArgumentException
	 *             if the connection is null or has not distinct endpoints
	 */
	void addConnection(ConnectionModelElement conn) throws Exception {
		if (conn == null || conn.getSource() == conn.getTarget()) {
			throw new IllegalArgumentException();
		}
		if (conn.getSource() == this) {
			addSourceConnection(conn);
		} else if (conn.getTarget() == this) {
			addTargetConnection(conn);
		}
	}
	
	protected void addSourceConnection(ConnectionModelElement conn) throws Exception{
		sourceConnections.add(conn);
		firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
		modelStructureChanged();
	}
	
	protected void addTargetConnection(ConnectionModelElement conn) throws Exception{
		targetConnections.add(conn);
		firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
		modelStructureChanged();
	}
	
	/**
	 * Remove an incoming or outgoing connection from this shape.
	 * 
	 * @param conn
	 *            a non-null connection instance
	 * @throws IllegalArgumentException
	 *             if the parameter is null
	 */
	void removeConnection(ConnectionModelElement conn) {
		if (conn == null) {
			throw new IllegalArgumentException();
		}
		if (conn.getSource() == this) {
			sourceConnections.remove(conn);
			firePropertyChange(SOURCE_CONNECTIONS_PROP, null, conn);
			modelStructureChanged();
		} else if (conn.getTarget() == this) {
			targetConnections.remove(conn);
			firePropertyChange(TARGET_CONNECTIONS_PROP, null, conn);
			modelStructureChanged();
		}
	}
	
	
	/**
	 * odswierza caly model (trzeba to zrobic, kiedy sie zmienily typy,
	 * np po polaczeniu/rozlaczeniu lub zmianie wlasciwosci
	 */
	public void modelStructureChanged() {
		if(isRefreshDiagramEnabled()){
			refreshDiagram(null);
			getDiagram().changed();
		}
	}
	
	private void refreshDiagram(Set<Shape> visited){
		if(isRefreshDiagramEnabled()){
			if(visited == null){
				visited = new HashSet<Shape>();
			}
			
			if(visited.contains(this)) return;
			else visited.add(this);
			
			firePropertyChange(SELF_PROP, null,new Object());
			for(ConnectionModelElement conn : sourceConnections){
				conn.getTarget().refreshDiagram(visited);
			}
			for(ConnectionModelElement conn : targetConnections){
				conn.getSource().refreshDiagram(visited);
			}
		}
	}
	
	/**
	 * Set the Location of this shape.
	 * 
	 * @param newLocation
	 *            a non-null Point instance
	 * @throws IllegalArgumentException
	 *             if the parameter is null
	 */
	public void setLocation(Point newLocation) {
		if (newLocation == null) {
			throw new IllegalArgumentException();
		}
		location.setLocation(newLocation);
		firePropertyChange(LOCATION_PROP, null, location);
	}

	/**
	 * Set the property value for the given property id. If no matching id is
	 * found, the call is forwarded to the superclass.
	 * <p>
	 * The property view uses the IDs from the IPropertyDescriptors array to set
	 * the values of the corresponding properties.
	 * </p>
	 * 
	 * @see #descriptors
	 * @see #getPropertyDescriptors()
	 */
	public void setPropertyValue(Object propertyId, Object value) {
		if (XPOS_PROP.equals(propertyId)) {
			int x = Integer.parseInt((String) value);
			setLocation(new Point(x, location.y));
		} else if (YPOS_PROP.equals(propertyId)) {
			int y = Integer.parseInt((String) value);
			setLocation(new Point(location.x, y));
		} else if (HEIGHT_PROP.equals(propertyId)) {
			int height = Integer.parseInt((String) value);
			setSize(new Dimension(size.width, height));
		} else if (WIDTH_PROP.equals(propertyId)) {
			int width = Integer.parseInt((String) value);
			setSize(new Dimension(width, size.height));
		} else {
			super.setPropertyValue(propertyId, value);
		}
	}

	/**
	 * Set the Size of this shape. Will not modify the size if newSize is null.
	 * 
	 * @param newSize
	 *            a non-null Dimension instance or null
	 */
	public void setSize(Dimension newSize) {
		if (newSize != null) {
			size.setSize(newSize);
			firePropertyChange(SIZE_PROP, null, size);
		}
	}
	
	public void load(Element elem) throws XMLLoaderException {
		if(elem!=null 
			&& elem.getChild("position")!=null ){
			Element pos = elem.getChild("position");
			setPropertyValue(XPOS_PROP, pos.getAttributeValue("x"));
			setPropertyValue(YPOS_PROP, pos.getAttributeValue("y"));
		}
	}
	
	public Element save() {
		Element graphics  = new Element("graphics");
		Element position = new Element("position");
		position.setAttribute("x",(String) getPropertyValue(XPOS_PROP));
		position.setAttribute("y",(String) getPropertyValue(YPOS_PROP));
		graphics.addContent(position);
		return graphics;
	}
}